package org.ligson.busdemo2;

import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import org.springframework.cloud.bus.event.UnknownRemoteApplicationEvent;
import org.springframework.cloud.bus.jackson.SubtypeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

@Configuration
public class ReferenceBusJacksonConfig {
    // needed in the case where @RemoteApplicationEventScan is not used
    // otherwise RemoteApplicationEventRegistrar will register the bean
    @Bean
    @Lazy(value = false)
    @Order(999)
    //@StreamMessageConverter
    //@ConditionalOnMissingBean(name = "busJsonConverter")
    public MessageConverter customMessageConverter() {
        ObjectMapper objectMapper = new ObjectMapper();


        return new CruxBusJacksonMessageConverter(objectMapper);
    }
}

class CruxBusJacksonMessageConverter extends AbstractMessageConverter
        implements InitializingBean {

    private static final Log log = LogFactory.getLog(CruxBusJacksonMessageConverter.class);

    private static final String DEFAULT_PACKAGE = ClassUtils
            .getPackageName(RemoteApplicationEvent.class);

    private final ObjectMapper mapper;

    private final boolean mapperCreated;

    private String[] packagesToScan = new String[]{DEFAULT_PACKAGE, "com.crux", "yonyougov","org.ligson"};

    private CruxBusJacksonMessageConverter() {
        this(null);
    }

    @Autowired(required = false)
    CruxBusJacksonMessageConverter(@Nullable ObjectMapper objectMapper) {
        super(MimeTypeUtils.APPLICATION_JSON);

        if (objectMapper != null) {
            this.mapper = objectMapper;
            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            this.mapperCreated = false;
        } else {
            this.mapper = new ObjectMapper();
            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            this.mapperCreated = true;
        }
    }


    /* for testing */ boolean isMapperCreated() {
        return this.mapperCreated;
    }

    /* for testing */ ObjectMapper getMapper() {
        return this.mapper;
    }

    public void setPackagesToScan(String[] packagesToScan) {
        List<String> packages = new ArrayList<>(Arrays.asList(packagesToScan));
        if (!packages.contains(DEFAULT_PACKAGE)) {
            packages.add(DEFAULT_PACKAGE);
        }
        this.packagesToScan = packages.toArray(new String[0]);
    }

    private Class<?>[] findSubTypes() {
        List<Class<?>> types = new ArrayList<>();
        if (this.packagesToScan != null) {
            for (String pkg : this.packagesToScan) {
                ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(
                        false);
                provider.addIncludeFilter(
                        new AssignableTypeFilter(RemoteApplicationEvent.class));

                Set<BeanDefinition> components = provider.findCandidateComponents(pkg);
                for (BeanDefinition component : components) {
                    try {
                        types.add(Class.forName(component.getBeanClassName()));
                    } catch (ClassNotFoundException e) {
                        throw new IllegalStateException(
                                "Failed to scan classpath for remote event classes", e);
                    }
                }
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("Found sub types: " + types);
        }
        return types.toArray(new Class<?>[0]);
    }

    @Override
    protected boolean supports(Class<?> aClass) {
        // This converter applies only to RemoteApplicationEvent and subclasses
        return RemoteApplicationEvent.class.isAssignableFrom(aClass);
    }

    @Override
    protected void addSupportedMimeTypes(MimeType... supportedMimeTypes) {
        super.addSupportedMimeTypes(MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_JSON);
    }

    @Override
    public List<MimeType> getSupportedMimeTypes() {
        List<MimeType> mimeTypes = new ArrayList<>();
        mimeTypes.add(MediaType.APPLICATION_OCTET_STREAM);
        mimeTypes.add(MediaType.APPLICATION_JSON);
        return mimeTypes;
    }

    @Override
    public Object convertFromInternal(Message<?> message, Class<?> targetClass,
                                      Object conversionHint) {
        Object result = null;
        try {
            Object payload = message.getPayload();

            if (payload instanceof byte[]) {
                try {
                    result = this.mapper.readValue((byte[]) payload, targetClass);
                } catch (InvalidTypeIdException e) {
                    logger.warn("1序列化消息异常:" + payload + ",原因:" + e.getMessage());
                    return new UnknownRemoteApplicationEvent(new Object(), e.getTypeId(),
                            (byte[]) payload);
                }
            } else if (payload instanceof String) {
                try {
                    result = this.mapper.readValue((String) payload, targetClass);
                } catch (InvalidTypeIdException e) {
                    logger.warn("2序列化消息异常:" + payload + ",原因:" + e.getMessage());
                    return new UnknownRemoteApplicationEvent(new Object(), e.getTypeId(),
                            ((String) payload).getBytes());
                }
                // workaround for
                // https://github.com/spring-cloud/spring-cloud-stream/issues/1564
            } else if (payload instanceof RemoteApplicationEvent) {
                return payload;
            }
        } catch (Exception e) {
            this.logger.error(e.getMessage(), e);
            return null;
        }
        return result;
    }

    private Class<?> extractViewClass(JsonView annotation, Object conversionHint) {
        Class<?>[] classes = annotation.value();
        if (classes.length != 1) {
            throw new IllegalArgumentException(
                    "@JsonView only supported for handler methods with exactly 1 class argument: " + conversionHint);
        }
        return classes[0];
    }


    @Nullable
    protected Class<?> getSerializationView(@Nullable Object conversionHint) {
        if (conversionHint instanceof MethodParameter) {
            MethodParameter param = (MethodParameter) conversionHint;
            JsonView annotation = (param.getParameterIndex() >= 0 ?
                    param.getParameterAnnotation(JsonView.class) : param.getMethodAnnotation(JsonView.class));
            if (annotation != null) {
                return extractViewClass(annotation, conversionHint);
            }
        } else if (conversionHint instanceof JsonView) {
            return extractViewClass((JsonView) conversionHint, conversionHint);
        } else if (conversionHint instanceof Class) {
            return (Class<?>) conversionHint;
        }

        // No JSON view specified...
        return null;
    }

    protected JsonEncoding getJsonEncoding(@Nullable MimeType contentType) {
        if (contentType != null && (contentType.getCharset() != null)) {
            Charset charset = contentType.getCharset();
            for (JsonEncoding encoding : JsonEncoding.values()) {
                if (charset.name().equals(encoding.getJavaName())) {
                    return encoding;
                }
            }
        }
        return JsonEncoding.UTF8;
    }

    @Override
    protected Object convertToInternal(Object payload, MessageHeaders headers, Object conversionHint) {
        try {
            Class<?> view = getSerializationView(conversionHint);
            if (byte[].class == getSerializedPayloadClass()) {
                ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
                JsonEncoding encoding = getJsonEncoding(getMimeType(headers));
                JsonGenerator generator = this.getMapper().getFactory().createGenerator(out, encoding);
                if (view != null) {
                    this.getMapper().writerWithView(view).writeValue(generator, payload);
                } else {
                    this.getMapper().writeValue(generator, payload);
                }
                payload = out.toByteArray();
            } else {
                Writer writer = new StringWriter();
                if (view != null) {
                    this.getMapper().writerWithView(view).writeValue(writer, payload);
                } else {
                    this.getMapper().writeValue(writer, payload);
                }
                payload = writer.toString();
            }
        } catch (IOException ex) {
            throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex);
        }
        return payload;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Class<?>[] subTypes = findSubTypes();
        StringBuilder builder = new StringBuilder();
        builder.append("[");
        for (Class<?> subType : subTypes) {
            builder.append(subType.getName()).append(",");
        }
        builder.append("]");
        logger.info("消息事件类:" + builder);
        this.mapper.registerModule(new SubtypeModule(subTypes));
    }

}
